Skip to main content

Exceptions, Try-Except Blocks, Custom Errors.

The following are frequently used in Python and a good understanding of each is required.

Exceptions

Exceptions are errors that occur whilst code is running and can be of numerous types. Some errors are caused by issues with the code, whilst others may be outside the developers control. You might get an error whilst trying to connect to a third party Python package, a file, or some external service, i.e. an API etc.

We'll start with looking at the different kinds of exceptions that can occur with some simple examples and then move on to how we can handle these errors.

Going back to one of our first simple pieces of Python code

teachers_age = 40
students_age = input("Please enter your age: ")
if students_age < teachers_age:
print("You are younger than your teacher")

Running the above will result in

TypeError: '<' not supported between instances of 'str' and 'int'

We are comparing the teachers_age, an integer, with the input from the user, students_age which is a string. To fix this particular error is easy, we just convert the input to an integer.

if int(students_age) < teachers_age

Let's see another simple example this time with a ValueError.

a_list = ["Richard", "TayFun", "Pablo"]
a_list.remove("Richard")
b_list = []
a_list.remove("Richard")

Run the above, and you get the following error and description.

ValueError: list.remove(x): x not in list

The above is telling us that the parameter for the list remove function is not in the list. The following are a few more examples of standard python errors thrown by issues with our code.

a_list = ["Richard", "TayFun", "Pablo"]
for i in a_list:
if my_name == i:
print(i)

This throws a NameError because my_name is not yet defined.

NameError: name 'my_name' is not defined
a_list = ["Richard", "TayFun", "Pablo"]
print(a_list[3])

This one throws an IndexError because we don't have 4 items in the list. Remember list are indexed by 0.

IndexError: list index out of range
my_name = "Richard"

def print_name():
print(my_name)
my_name = "Tim"
print(my_name)

print_name()

This one is a little trickier. The variable my_name is first set outside the function print_name, so it's global accessible to all the code. But, in this case, we are redefining it after the first print statement in the function print_name. This is what causes the error. Python now sees this variable as local to the function and is trying to access it before it has been locally defined. You'll get the following error for this

UnboundLocalError: local variable 'my_name' referenced before assignment

Remove that first print statement from the function, and you won't get an error because the global my_name is redefined locally by the statement my_name = "Tim".

Another common error is a KeyError. This is thrown when trying to access a key that does not exist in a dictionary object.

cars = {"Audi": "German", "BMW": "German", "Land Rover": "English"}
mercedes = cars["Mercedes"]

The result

KeyError: 'Mercedes'

The AttributeError can also be a common visitor especially when learning Python. It occurs when you try to do something with an object using some built in method.

x = "something"
print(x.upper())
x.append(" else")
AttributeError: 'str' object has no attribute 'append'

The next example will throw a FileNotFoundException when trying to open a file that does not exist.

def read_json_file(json_file):

import json

# Opening JSON file
with open(json_file) as jf:

# returns JSON object as
# a dictionary
data = json.load(jf)

return data


data = read_json_file('../starwars-data/people.txt')
FileNotFoundError: [Error no 2] No such file or directory: '../starwars-data/people.txt'

Now we have a good idea about some of the more common errors that can occur. Our next step is to see how we handle these errors in our code.

For a full list of Python Errors see Python Built-in Exceptions

Try-Except Blocks

Python try-except blocks offer a method of running code that may fail and fall back on a solution. The try part of the block attempts to run some code, whilst the except block catches the failure of that code and its cause, an 'Exception', and handles that failure with some more code, usually by handling the exception in some way or some other method defined by us the developers.

Let's, look at the first error we defined in the last section, but with a twist, we'll change the error that occurs to a ValueError because we wouldn't forget to convert the input to an integer, would we? "Smile"

teachers_age = 40
students_age = input("Please enter your age: ")
try:
if int(students_age) < teachers_age:
print("You are younger than your teacher")
else:
print("You are older than your teacher, but don't tell anyone.")
except ValueError as e:
print("An error in the input occured - make sure that you input a number only")

Run the code above, input some characters, anything but not a number and see what happens.

You'll see that we have handled the incorrect input from the user without the code failing with an ugly error output. We have handled the error gracefully. It may be the case where there is potential for more than one type of error to occur. Using 'try-except' blocks we can handle different errors types in the same block.

If we take away the integer conversion of the user input we will force a TypeError.

teachers_age = 40
students_age = input("Please enter your age: ")
try:
if students_age < teachers_age:
print("You are younger than your teacher")
else:
print("You are older than your teacher, but don't tell anyone.")
except ValueError:
print("An error in the input occurred - make sure that you input a number only")
except TypeError as e:
print(f"An error in the code occurred {e} - Please inform the system administrator")

The above code catches the TypeError regardless of its position in the except order. However, there is one slight difference in the except block for the TypeError. We are assigning the error to a variable with the statement as. What this does, is put the error object and any elements such as an error message into the variable e. In this case, e will translate to a string of why the error occurred. We then include that string in our error explanation.

Depending on the error there may be different components to the error such as a message or an http status code if the error occurred calling an external service. These components are accessible through dot notation such as e.message or e.status. Useful for checking how we want to respond to the error.

Try-except blocks also provide a finally statement that is used after the try and except blocks occur, regardless of whether an error has occurred or not.

teachers_age = 40
students_age = input("Please enter your age: ")
try:
if int(students_age) < teachers_age:
print("You are younger than your teacher")
else:
print("You are older than your teacher, but don't tell anyone.")
except ValueError:
print("An error in the input occurred - make sure that you input a number only")
except TypeError as e:
print(f"An error in the code occurred {e} - Please inform the system administrator")
finally:
print("Code Block Completed")
Custom Errors

Errors can be confusing to those without technical knowledge, When we write our own code we might not want to just pass on the errors that occur as is to a user or a client.
It is often better to tailor our responses to errors in non-technical phrasing. Even the error types can sometimes be difficult to understand for developers that are using an API that has not considered the user-friendliness of its error messages.

To receive a multitude of different raw error types would be annoying at least and demand a lot more error handling on the users side at worst.

Say we have developed an API to provide some data service over the internet. Occasionally, the requests for such data may be badly put together or even misspelled. Believe it or not it happens. As Developers of a service or any codebase for that matter it is our job to make the responses to faulty requests as painless as possible.

Ok, deep breath!

Below is an example of using a third party package requests to make requests to an external service that provides information on the movie Star Wars. This service is free to use.

What we are doing in this code is calling an URL (uniform resource locator/web service address) with a query that indicates what data we are requesting. If the query is correct it should return the data. I say should here because anything can happen between the request and response, including the loss of internet service, and we need to cover those eventualities in our error handling. The package requests has its own exceptions which we must catch and deal with. However, when we catch those we will not pass those directly back to our users, instead we define our own error handler, in this case called ApiError and use this to communicate errors to the users.

Study the code below and then run it.

import requests
import json

URL = 'https://swapi.py4e.com/api/'

def request_data_sync(query):
"""
Request and wait for our data to return
In this method we are using the requests package to make a simple synchronous API call

:param query: Contains query parameters for the request
:return:
"""
status = ""

try:
# Format the URL from the main swAPI URL plus the query/queries
URL = f"{URL}{query}/"
# make the request
r = requests.get(url=url)
# Raise the status to make sure it was successful. If it is not the below exception will occur
status = r.status_code
r.raise_for_status()

# We have success - let's return the data
# extracting data in JSON format
return r.json()

except requests.ConnectionError:
msg = "OOPS!! Connection Error. Make sure you are connected to a live Internet connection."
raise ApiError(message=msg, status_code=status)
except requests.Timeout:
msg = "timeout-error"
raise ApiError(message=msg, status_code=status)
except requests.HTTPError:
if status == 404:
msg = "not-found"
elif status == 400:
msg = "bad-request"
elif status == 500:
msg = "server-error-star-wars-api"
else:
msg = "something-went-wrong"
raise ApiError(message=msg, status_code=status)
except KeyboardInterrupt:
msg = "program-closed"
raise ApiError(message=msg, status_code=status)
except Exception as e:
msg = str(e)
# If there is a status code send it with the message if not just send the message and a default status of 500 will be applied in the ApiError class
raise ApiError(message=msg, status_code=status) if status else ApiError(message=msg)


class ApiError(Exception):
"""
Parent Error Class - inherits default Exception
:param: Exception - The raised exception
"""
def __init__(self, message='There was an error', status_code=500, payload=None):
"""
Class
:param message: String
:param status_code: Integer
:param payload: Dict
"""
Exception.__init__(self)
self.message = message

if status_code is not None:
self.status_code = status_code
self.response = {'status_code': status_code, 'message': message}

def __str__(self):
return json.dumps(self.response)

# Ask for data on the first Star Wars movie in the series
result = request_data_sync('films/1')
print(result)

Run the above as is, and you should see the requested data printed out. Don't worry about the format of the data, it arrived that is what is important.

Now change the 1 in the query films/1 to 20 and run it again.

The last line of the output in the console should be

__main__.ApiError: {"status_code": 404, "message": "not-found"}

This shows that we have our own error handler dealing with the errors. Of course in reality we would send this error to the client in a response.

Let's try another error, change the 20 back to 1 i.e. films/1 and then go to the top of the code and take the colon : away from the https: so you have https.

Now run it again

This time the last line of the error output should look like this

_main__.ApiError: {"status_code": 500, "message": "Invalid URL 'https//swapi.py4e.com/api/films/1/': No scheme supplied. Perhaps you meant http://https//swapi.py4e.com/api/films/1/?"}

This error was caught by our catchall exception at the end of our exceptions block.

except Exception as e:
msg = str(e)
# If there is a status code send it with the message if not just send the message and a default status of 500 will be applied in the ApiError class
raise ApiError(message=msg, status_code=status) if status else ApiError(message=msg)

That's it for this section, you can move on to the next part of this tutorial.